function flvParse (arrayBuffer) {
    const header = []
    const body = []
    let dataOffset
    
    const data = new Uint8Array(arrayBuffer)

    // header: flv, version
    header.push(
        {
            field: 'Signature',
            type: 'UI8',
            value: data[0]
        },
        {
            field: 'Signature',
            type: 'UI8',
            value: data[1]
        },
        {
            field: 'Signature',
            type: 'UI8',
            value: data[2]
        },
        {
            field: 'Version',
            type: 'UI8',
            value: data[3]
        }
    )

    // header: TypeFlags
    const typeFlags = data[4]
    header.push(
        {
            field: 'TypeFlagsReserved',
            type: 'UB[5]',
            value: (typeFlags >> 3) | 0
        },
        {
            field: 'TypeFlagsAudio',
            type: 'UB[1]',
            value: (typeFlags >> 2) & 1
        },
        {
            field: 'TypeFlagsReserved',
            type: 'UB[1]',
            value: (typeFlags >> 1 ) & 1
        },
        {
            field: 'TypeFlagsVideo',
            type: 'UB[1]',
            value: typeFlags & 1
        }
    )

    // header: DataOffset    
    let view = new DataView(arrayBuffer, 5, 4)
    dataOffset = view.getUint32(0)
    header.push(
        {
            field: 'DataOffset',
            type: 'UI32',
            value: dataOffset
        }
    )

    // const body = arrayBuffer.slice(9)
    // body
    window.uint8 = data
    for (let offset = dataOffset; data[offset] !== undefined;) {
        const previousTagSize = readBytes32(data, offset)
        
        const tagOffset = offset + 4
        const tagType = data[tagOffset]
        const dataSize = readBytes24(data, tagOffset + 1)
        const timeStamp = readBytes24(data, tagOffset + 1 + 3)
        const timeStampExtended = data[tagOffset + 1  + 3 + 3]
        const streamID = readBytes24(data, tagOffset + 1 + 3 + 3 + 1)

        const dataOffset = tagOffset + 1 + 3 + 3 + 1 + 3
        // const streamData = data.slice(tagOffset + 1 + 3 + 3 + 1 + 3, dataSize)
        const parsedStreamData = parseStreamData(data, tagType, dataOffset, dataSize)


        // previous tag size
        body.push({
            field: 'PreviousTagSize',
            size: previousTagSize
        })

        // flv tag
        body.push({
            field: 'Tag',
            TagType: tagType,
            DataSize: dataSize,
            TimeStamp: timeStamp,
            TimeStampExtended: timeStampExtended,
            StreamID: streamID,
            StreamData: parsedStreamData
        })

        offset += (11 + dataSize + 4)
    }


    return {
        header,
        body
    }
}

function readBytes32 (array, start) {
    return (
        (array[start] << 24) |
        (array[start + 1] << 16) |
        (array[start + 2] << 8) |
        (array[start + 3])
    )
}

function readBytes24 (array, start) {
    return (
        (array[start] << 16) |
        (array[start + 1] << 8) |
        (array[start + 2])
    )
}

function readSI24 (array, start) {
    const ui23 = (
        ((array[start] & 0x7F) << 16) |
        (array[start + 1] << 8) |
        (array[start + 2])
    )
    const sign = array[start] >>> 7
    return sign === 1 ? -ui23 : ui23
    
}

let print = 0

function parseStreamData (array, tagType, offset, dataSize) {
    let ret
    if (tagType === 8) {
        // audio data

        const soundFormat = array[offset] >>> 4
        const soundRate = (array[offset] >>> 2) & 3 
        const soundSize = (array[offset] >>> 1) & 1
        const soundType = array[offset] & 1
        
        let AACDataDetail = null
        if (soundFormat === 10) {
            // parse AAC
            AACDataDetail = {}
            const AACPackageType = array[offset + 1]
            AACDataDetail.AACPackageType = AACPackageType
            if (AACPackageType === 0) {
                AACDataDetail.AACPackageTypeTitle = 'AAC Sequence header'
                AACDataDetail.DataTitle = 'AudioSpecificConfig'
            } else if (AACPackageType === 1) {
                AACDataDetail.AACPackageTypeTitle = 'AAC raw'
                AACDataDetail.DataTitle = 'Raw AAC frame data'
            } else {
                AACDataDetail.AACPackageTypeTitle = 'Unknown'
            }
        }

        ret = {
            TagType: 'audio',
            SoundFormat: soundFormat,
            SoundFormatTitle: getSoundFormatTitle(soundFormat),
            SoundRate: soundRate,
            SoundRateTitle: getSoundRateTitle(soundRate),
            SoundSize: soundSize,
            SoundSizeTitle: getSoundSizeTitle(soundSize),
            SoundType: soundType,
            SoundTypeTitle: getSoundTypeTitle(soundType),
            AACDataDetail: AACDataDetail
        }
    } else if (tagType === 9) {
        // video data

        const frameType = array[offset] >>> 4
        const codecID = array[offset] & 15
        
        let avcDetail = null

        if (codecID === 7) {
            const AVCPackageType = array[offset + 1]
            const AVCPackageTypeTitle = getAVCPackageTypeTitle(AVCPackageType)
            const CompositionTime = readSI24(offset + 1 + 1)
            const CompositionTimeTitle = AVCPackageType === 1 ? 'Composition time offset' : '0'
            const DataTitle = getAVCDataTitle(AVCPackageType)
            avcDetail = {
                AVCPackageType,
                AVCPackageTypeTitle,
                CompositionTime,
                CompositionTimeTitle,
                DataTitle
            }
        }

        ret = {
            TagType: 'video',
            FrameType: frameType,
            FrameTypeTitle: getVideoFrameTypeTitle(frameType),
            CodecID: codecID,
            CodecIDTitle: getVideoCodecIDTitle(codecID),
            avcDetail: avcDetail
        }

    } else if (tagType === 18) {
        // script data object
        ret = {
            TagType: 'script data'
        }
        const end = readBytes24(array, offset + dataSize - 3)

    } else {
        // unknown data

        ret = {
            TagType: 'unknown'
        }
    }
    return ret
}

function getSoundFormatTitle (format) {
    let type
    switch (format) {
        case 0:
            type = 'Linear PCM,platform endian'
            break;
        case 1:
            type = 'ADPCM'
            break;
        case 2:
            type = 'MP3'
            break;
        case 3:
            type = 'Linear PCM,little endian'
            break;
        case 4:
            type = 'Nellymoser 16-kHZ mono'
            break;
        case 5:
            type = 'Nellymoser 8-kHZ mono'
            break;
        case 6:
            type = 'Nellymoser'
            break
        case 7:
            type = 'G.711 A-law logarithmic PCM'
            break;
        case 8:
            type = 'G.711 mu-law logarithmic PCM'
            break;
        case 9:
            type = 'reserved'
            break;
        case 10:
            type = 'AAC'
            break;
        case 11:
            type = 'Speex'
            break;
        case 14:
            type = 'MP3 8-khz'
            break;
        case 15:
            type = 'Device-specific sound'
            break
        default:
            type = 'Unknown'
            break;
    }
    return type
}

function getSoundRateTitle (rate) {
    var ret 
    switch (rate) {
        case 0:
            ret = '5.5-kHz'
            break;
        case 1:
            ret = '11-kHz'
            break
        case 2:
            ret = '22-kHz'
            break
        case 3:
            ret = '44-kHz'
            break
        default:
            ret = 'Unknown'
            break;
    }
    return ret
}

function getSoundSizeTitle (size) {
    return size === 0 ? 'snd8Bit' : 'snd16Bit'
}

function getSoundTypeTitle (type) {
    return type === 0 ? 'sndMono' : 'sndStereo'
}

function getVideoFrameTypeTitle (type) {
    let ret
    switch (type) {
        case 1:
            ret = 'keyframe(for AVC, a seekable frame)'
            break;
        case 2:
            ret = 'interframe(for AVC, a nonseekable frame)'
            break;
        case 3:
            ret = 'disposable inter frame(reserved for server use only)'
            break;
        case 4:
            ret = 'generated keyframe(reserved for server use only)'
            break;
        case 5:
            ret = 'video info/command frame'
            break
        default:
            ret = 'Unknown'
            break;
    }
    return ret
}

function getVideoCodecIDTitle (codec) {
    let title
    switch (codec) {
        case 1:
            title = 'JPEG (currently unused)'
            break;
        case 2:
            title = 'Sorenson H.263'
            break;
        case 3:
            title = 'Screen video';
            break;
        case 4:
            title = 'On2 VP6';
            break;
        case 5:
            title = 'On2 VP6 with alpha channel'
            break;
        case 6:
            title = 'Screen video width version 2'
            break;
        case 7:
            title = 'AVC'
            break;
        default:
            title = 'Unknown'
            break;
    }
    return title
}

function getAVCPackageTypeTitle (type) {
    let title
    switch (type) {
        case 0:
            title = 'AVC Sequence header'
            break;
        case 1:
            title = 'AVC NALU'
            break;
        case 2:
            title = 'AVC end of sequence (low level NALU sequence ender is not required or supported)'
            break;
        default:
            title = 'Unknown'
            break;
    }
    return title
}

function getAVCDataTitle (type) {
    let title
    switch (type) {
        case 0:
            title = 'AVCDecoderConfigurationRecord'
            break;
        case 1:
            title = 'One or more NALUs (can be individual slices per FLV packets; that is, full frames are not strictly required)'
            break;
        case 2:
            title = 'Empty'
        default:
            title = 'Unknown'
            break;
    }
    return title
}